#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <limits.h>

#include <arpa/inet.h>

#include "FtpClient.h"

#define PACKET_SIZE (150)
#define HEADER_SIZE (sizeof(FtpCmdHeader))
#define OUT
#define BUFFER_SIZE (1500)

typedef enum FtpEvent_t
{
    FTP_EVENT_ERR   = 0      ,
    FTP_EVENT_USR            ,
    FTP_EVENT_PSWD           ,
    FTP_EVENT_CD             ,
    FTP_EVENT_LS             ,
    FTP_EVENT_BYE            ,
    FTP_EVENT_GET            ,
    FTP_EVENT_PUT            ,
    FTP_EVENT_NUM_OF_EVENTS
}FtpEvent;


typedef enum FtpClientErrStr_t
{
    FTP_CL_SUCCESS          ,
    FTP_CL_ERR_FILE_TRANSFER
}FtpClientErrStr;

char* ftpErrorStr[] = {
                        "",
                        "ERROR: error occured while transfering file"
};

/*  The general header struct   */
typedef struct FtpCmdHeader_t
{
    union                                           /*  anonnonymus union for network purpose   */
    {
        struct
        {
            unsigned int    m_dataSize : 24 ;
            unsigned int    m_errNum   : 8  ;       /*  Error number                            */
        }u_realHead;

        unsigned int    u_innerHeaderAsInt;         /*  member of union for htonl       */
    }m_innerHeader;

}FtpCmdHeader;

/*  struct for manage inner header for file transfer    */
typedef struct FtpDataHeader_t
{
    union
    {
        struct
        {
            unsigned int      m_pos    : 31;      /*  The position in file                    */
            unsigned int      m_isLast : 1;       /*  flag inidicate if it's the last packet  */
        }u_realDataHead;

        unsigned int    u_innerHeaderAsInt;         /*  member of union for htonl       */
    }m_innerDataHeader;

}FtpDataHeader;


struct FtpClient_t
{
    GenClient* m_gClient;
    char*      m_headBuf;
    char*      m_recvBuf;
    void*      m_state;
};

/*  Function for adding for given buffer header in sizeof(FtpCmdHeader) */
static void AddFtpCmdHeader(void* OUT _buffer, size_t _size, int _errNum)
{
    FtpCmdHeader* fHeader = 0;

    assert(_buffer);

    fHeader = (FtpCmdHeader*)_buffer;
    fHeader->m_innerHeader.u_realHead.m_dataSize = _size;
    fHeader->m_innerHeader.u_realHead.m_errNum   = _errNum;

    fHeader->m_innerHeader.u_innerHeaderAsInt = htonl(fHeader->m_innerHeader.u_innerHeaderAsInt);
}


void AddFtpDataHeader(void* OUT _buffer, int _isLast, size_t _pos)
{
    FtpDataHeader* fHeader = 0;

    assert(_buffer);

    fHeader = (FtpDataHeader*)_buffer;
    fHeader->m_innerDataHeader.u_realDataHead.m_isLast = _isLast;
    fHeader->m_innerDataHeader.u_realDataHead.m_pos    = _pos;
    fHeader->m_innerDataHeader.u_innerHeaderAsInt = htonl(fHeader->m_innerDataHeader.u_innerHeaderAsInt);   /*  using the union to switch format to network */
}

/*  Send error message to server according to error number  */
int SendError(GenClient* _client, int _errNum)
{
    char    sendBuf[BUFFER_SIZE];
    size_t  strSize;

    assert(_client);

    strSize = strlen(ftpErrorStr[_errNum]); /*  the size of the error string    */
    AddFtpCmdHeader(sendBuf, strSize, _errNum);
    memcpy(sendBuf + sizeof(FtpCmdHeader), ftpErrorStr[_errNum], strSize);
    GenClientWrite(_client, sendBuf, sizeof(FtpCmdHeader) + strSize);

    return 0;
}

/*  Send data of file   */
void SendFileData(GenClient* _client, char* OUT _buffer, size_t _dataSize, size_t _pos, int _isLast, int _errNum)
{
    AddFtpCmdHeader(_buffer, _dataSize + sizeof(FtpDataHeader), _errNum);
    AddFtpDataHeader(_buffer + sizeof(FtpCmdHeader), _isLast, _pos);
    GenClientWrite(_client, _buffer, sizeof(FtpCmdHeader) + sizeof(FtpDataHeader) + _dataSize);
}


void EchoFile(FtpClient* _fClient, char* _buf)
{
    char    recvBuf[BUFFER_SIZE];
    int     status;
    size_t  msgLen = FtpClientRecv(_fClient, recvBuf, &status);

    printf("The status: %d\n", status);
    while(!status)
    {
        *(recvBuf + msgLen) = '\0';
        printf("%s", recvBuf + sizeof(FtpDataHeader));
        if(((FtpDataHeader*)recvBuf)->m_innerDataHeader.u_realDataHead.m_isLast == 1)
        {
            break;
        }
        msgLen = FtpClientRecv(_fClient, recvBuf, &status);
    }
}


void EchoMssg(FtpClient* _fClient, char* _buf)
{
    char recvBuf[BUFFER_SIZE];
    int  status;
    size_t msgLen = FtpClientRecv(_fClient, recvBuf, &status);

    *(recvBuf + msgLen) = '\0';
    printf("%s\n", recvBuf);
}


int FtpClientErr(FtpClient* _fClient, void* _param)
{
    EchoMssg(_fClient, (char*)_param);
    return 0;
}

int FtpClientPswd(FtpClient* _fClient, void* _param)
{
    EchoMssg(_fClient, (char*)_param);
    return 0;
}


int FtpClientGet(FtpClient* _fClient, void* _param)
{
    FILE*       fp                          ;
    char        recvBuf[BUFFER_SIZE]        ;
    int         status                      ;
    size_t      msgLen                      ;
    size_t      totBytesArrived = 0         ;
    size_t      fileLen         = ULONG_MAX ;   /*  Init file length to maximum (until the last arrive and then the length will updated)    */

    assert(_fClient);
    assert(_param);

    msgLen = FtpClientRecv(_fClient, recvBuf, &status);
    if(status)                                          /*  Check there isn't any error     */
    {
        *(recvBuf + msgLen) = '\0';
        printf("%s\n", recvBuf);
        return -1;
    }

    if(0 == (fp = fopen(_param, "w+")))                 /*  Open the file for write         */
    {
        perror("fopen");
        return -1;
    }
    totBytesArrived += msgLen - sizeof(FtpDataHeader);  /*  Counter for the bytes arrived   */

    while(!status)                                      /*  While not error occured         */
    {
        ((FtpDataHeader*)recvBuf)->m_innerDataHeader.u_innerHeaderAsInt = ntohl(((FtpDataHeader*)recvBuf)->m_innerDataHeader.u_innerHeaderAsInt);   /*  using the union to switch format to network */
        fseek(fp, ((FtpDataHeader*)recvBuf)->m_innerDataHeader.u_realDataHead.m_pos, SEEK_SET);  /*  Go to the right position in file    */
        if(fwrite(recvBuf + sizeof(FtpDataHeader), 1,  msgLen - sizeof(FtpDataHeader), fp) < msgLen - sizeof(FtpDataHeader))
        {
            perror("fwrite error");
            fclose(fp);
            return -1;
        }

        if(((FtpDataHeader*)recvBuf)->m_innerDataHeader.u_realDataHead.m_isLast == 1)
        {
            fileLen = msgLen - sizeof(FtpDataHeader) + ((FtpDataHeader*)recvBuf)->m_innerDataHeader.u_realDataHead.m_pos;
        }
        if(totBytesArrived >= fileLen)
        {
            break;
        }
        msgLen = FtpClientRecv(_fClient, recvBuf, &status);
        totBytesArrived += msgLen - sizeof(FtpDataHeader);  /*  Counter for the bytes arrived   */
    }

    if(status)                                              /*  If error occured - prints it    */
    {
        *(recvBuf + msgLen) = '\0';
        printf("%s\n", recvBuf);
    }
    fclose(fp);

    return 0;
}


int FtpClientCd(FtpClient* _fClient, void* _param)
{
    EchoMssg(_fClient, 0);

    return 0;
}

int FtpClientLs(FtpClient* _fClient, void* _param)
{
    char        recvBuf[BUFFER_SIZE]        ;
    int         status                      ;
    size_t      msgLen                      ;
    size_t      totBytesArrived = 0         ;
    size_t      fileLen         = ULONG_MAX ;   /*  Init file length to maximum (until the last arrive and then the length will updated)    */

    assert(_fClient);
    assert(_param);


    msgLen = FtpClientRecv(_fClient, recvBuf, &status);
    if(status)                                          /*  Check there isn't any error     */
    {
        *(recvBuf + msgLen) = '\0';
        printf("%s\n", recvBuf);
        return -1;
    }

    totBytesArrived += msgLen - sizeof(FtpDataHeader);  /*  Counter for the bytes arrived   */

    while(!status)                                      /*  While not error occured         */
    {
        *(recvBuf + msgLen) = '\0';
        printf("%s", recvBuf + sizeof(FtpDataHeader));
        ((FtpDataHeader*)recvBuf)->m_innerDataHeader.u_innerHeaderAsInt = ntohl(((FtpDataHeader*)recvBuf)->m_innerDataHeader.u_innerHeaderAsInt);   /*  using the union to switch format to host */
        if(((FtpDataHeader*)recvBuf)->m_innerDataHeader.u_realDataHead.m_isLast == 1)
        {
            fileLen = msgLen - sizeof(FtpDataHeader) + ((FtpDataHeader*)recvBuf)->m_innerDataHeader.u_realDataHead.m_pos;
        }
        if(totBytesArrived >= fileLen)
        {
            break;
        }
        msgLen = FtpClientRecv(_fClient, recvBuf, &status);
        totBytesArrived += msgLen - sizeof(FtpDataHeader);  /*  Counter for the bytes arrived   */
    }

    if(status)                                              /*  If error occured - prints it    */
    {
        *(recvBuf + msgLen) = '\0';
        printf("%s\n", recvBuf);
    }


    return 0;
}

int FtpClientBye(FtpClient* _fClient, void* _param)
{
    EchoMssg(_fClient, 0);

    return FCERR_LOGOUT;
}

int FtpClientPut(FtpClient* _fClient, void* _param)
{
    char*       sendBuf     = 0 ;
    size_t      bytesRead       ;
    size_t      position    = 0 ;
    size_t      msgLen          ;
    FILE*       fp          = 0 ;
    int         isLast      = 0 ;
    int         status          ;
    char        buf[BUFFER_SIZE];

    assert(_param);

    if(0 == (fp = fopen((char*)_param, "r")))
    {
        printf("The file %s not exist\n", (char*)_param);

        return -1;
    }

    sendBuf = (char*)malloc(PACKET_SIZE );     /*  Allocate buffer in packet size for send */
    if(!sendBuf)
    {
        perror("allocate send buffer");
        fclose(fp);
        return -1;
    }

    strcpy(buf + HEADER_SIZE, "put ");
    strcat(buf + HEADER_SIZE, _param);
    printf("%s\n", buf + HEADER_SIZE);
    AddFtpCmdHeader(buf, strlen(buf + HEADER_SIZE) + 1, 0);
    GenClientWrite(_fClient->m_gClient ,buf, strlen(buf + HEADER_SIZE) + 1 + HEADER_SIZE);

    msgLen = FtpClientRecv(_fClient, buf, &status);
    *(buf + msgLen) = '\0';
    printf("%s\n", buf);
    if(status)
    {
        return -1;
    }

    while(!feof(fp))                        /*  Loop for reading from file          */
    {
        bytesRead = fread(sendBuf + sizeof(FtpCmdHeader) + sizeof(FtpDataHeader), 1, PACKET_SIZE - sizeof(FtpCmdHeader) - sizeof(FtpDataHeader), fp);    /*  read size of packet - headers   */
        if(feof(fp))                        /*  If this the last packet             */
        {
            isLast = 1;
        }
        else if(ferror(fp))                 /*  If error occured send error message */
        {
            /*SendError(_fClient->m_gClient, FTP_CL_ERR_FILE_TRANSFER);*/
            break;
        }
        SendFileData(_fClient->m_gClient, sendBuf, bytesRead, position, isLast, FTP_CL_SUCCESS);
        position += bytesRead;              /*  increment position in file          */
    }
    fclose(fp);
    free(sendBuf);

    return 0;
}


FtpClient* FtpClientNew(char* _ip, int _port, int _protocol)
{
    FtpClient* fClient = (FtpClient*)malloc(sizeof(FtpClient));
    if(!fClient)
    {
        perror("error on allocating FTP client");
    }

    fClient->m_gClient = GenClientNew(_ip, _port, _protocol);
    if(!fClient->m_gClient)
    {
        fprintf(stderr, "error on creating new generic client\n");
    }
    EchoMssg(fClient, 0);   /*  Get ack for the connection  */

    return fClient;
}




int FtpClientUser(FtpClient* _fClient, void* _buf)
{
    EchoMssg(_fClient, _buf);

    return 0;
}


int FtpParser(void* _buffer, size_t _size, int(**_cmdImp)(FtpClient* , void* ), char* OUT _param)
{
    int     i                                       ;
    char*   cp                                  = 0 ;
    int(*cmdImp[])(FtpClient* , void* ) = {FtpClientErr ,FtpClientUser, FtpClientPswd, FtpClientBye, FtpClientLs, FtpClientCd, FtpClientGet, FtpClientPut};
    char*   ftpEvents[FTP_EVENT_NUM_OF_EVENTS]  = {"","user", "password", "bye", "ls","cd" , "get", "put"};
    char*   buf                                 = (char*)malloc(_size);

    memcpy(buf, _buffer, _size);    /*  Copy buffer         */
    cp = strchr(buf, ' ');          /*  Get ptr to space    */
    if(!cp)
    {
        *_param = '\0';             /*  No param            */
    }
    else
    {
        *cp = '\0';                 /*  Replace space with null terminated  */
        strcpy(_param, cp + 1);     /*  copy the param to output parameters */
    }


    for(i = 1; i < FTP_EVENT_NUM_OF_EVENTS; ++i)
    {
        if(!strcmp(buf, ftpEvents[i]))              /*  Find the match command  */
        {
            *_cmdImp = cmdImp[i];
            break;
        }
    }
    if(i == FTP_EVENT_NUM_OF_EVENTS)                /*  Wrong cmd               */
    {
        *_cmdImp = cmdImp[0];
    }
    free(buf);

    return 0;
}


int FtpClientExecute(FtpClient* _fClient, char* _buf, size_t _size)
{
    int(*cmd)(FtpClient* , void*);
    char param[200];
    char* buf = 0;

    buf = malloc(_size + HEADER_SIZE);
    if(!buf)
    {
        perror("error on allocating FtpClient buffer");
    }


    FtpParser(_buf, _size, &cmd, param);
    if(cmd != FtpClientPut)               /*  Put need diffrent care  */
    {
        AddFtpCmdHeader(buf, _size, 0);
        memcpy(buf + HEADER_SIZE, _buf, _size);
        GenClientWrite(_fClient->m_gClient, buf, _size + HEADER_SIZE);
    }

    return (*cmd)(_fClient, param); /*  activate function   */
}


size_t AnalyzeHeader(void* _header, int* _status)
{
    ((FtpCmdHeader*)_header)->m_innerHeader.u_innerHeaderAsInt = ntohl(((FtpCmdHeader*)_header)->m_innerHeader.u_innerHeaderAsInt);

    *_status = ((FtpCmdHeader*)_header)->m_innerHeader.u_realHead.m_errNum;
    return ((FtpCmdHeader*)_header)->m_innerHeader.u_realHead.m_dataSize;
}


/*  Recieve header and data. On header active header analyze function and on data the relavant state func   */
int FtpClientRecv(FtpClient* _fClient, void* _buf, int* _status)
{
    size_t sizeOfData;

    GenClientRead(_fClient->m_gClient, _buf, HEADER_SIZE);

    sizeOfData = AnalyzeHeader(_buf, _status);
    GenClientRead(_fClient->m_gClient, _buf, sizeOfData);

    return sizeOfData;
}

